package com_.dxy_test_.nio_.file_;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;

/**
 * @Description NIO方式文件读写
 * <p>
 * NIO是基于缓冲区的，读写数据直接对接的是缓冲区Buffer，读数据时直接从Buffer中读取，写数据时直接写入到Buffer中
 * <p>
 * IO设备和Buffer之间，Buffer和用户程序之间，通过通道Channel连接，每个Channel对应一个Buffer
 * <p>
 * 这个缓冲区和BIO（如BufferedReader）中的缓冲区是不一样的
 * <p>
 * BIO中的缓冲区只是一个简陋的byte[]数组或char[]数组
 * <p>
 * 而NIO的缓冲区是指Buffer类，有mark，position，limit，capacity四个重要属性
 * <p>
 * NIO技术精讲之缓冲区Buffer：
 * https://blog.csdn.net/CHINACR07/article/details/103641324
 * <p>
 * NIO中 FileChannel不能设置为非阻塞，总是阻塞的
 * <p>
 * Java NIO全面详解(看这篇就够了)
 * https://www.cnblogs.com/mikechenshare/p/16587635.html
 * @Author dangxianyue
 * @Date 2022/11/5 15:55
 */
public class FileReadWriteTest {
    public static void main(String[] args) {
//        readTest();
//        writeTest();
//        copyTest();
        zeroCopyForMmapTest();
//        zeroCopyForSendfileTest();
    }


    /**
     * 读文件
     */
    private static void readTest() {
        String filePath = "C:\\Users\\Alex\\Desktop\\app\\read.txt";
        FileChannel channel = null;//文件通道，有点像流，但流是单向的，通道是双向的
        try {
            //打开通道
            //另外也可以通过FileInputStream.getChannel()获取
            channel = new RandomAccessFile(filePath, "rw").getChannel();

            //声明字节缓冲区，每个Channel一个Buffer
            //allocate声明的是创建在JVM堆中的缓冲区，HeapByteBuffer
            //allocateDirect声明的是创建在堆外，声明在系统内存中的缓冲区DirectByteBuffer，是MappedByteBuffer内存映射的子类，在超大文件效率较快
            //DirectByteBuffer通过免去中间交换的内存拷贝也属于zero-copy范畴
            //ByteBuffer buffer = ByteBuffer.allocate(10);
            ByteBuffer buffer = ByteBuffer.allocateDirect(16);

            //读取数据
            StringBuilder text = new StringBuilder();
            int line = 0;
            while (channel.read(buffer) != -1) {// 读取通道中的数据，并写入到缓冲区中
                buffer.flip();//缓存区切换到读模式，类似于flush
                while (buffer.position() < buffer.limit()) {
                    char c = (char) buffer.get();
                    text.append(c);
                    if ('\n' == c) {
                        System.out.println("读完第" + (++line) + "行！");
                    }
                }
                buffer.clear();//清空 buffer，缓存区切换到写模式，继续从通道中写入下一批数据
            }

            System.out.println(text);

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //关闭通道，关闭了FileChannel后不需要手动关闭RandomAccessFile
            //详见：https://blog.csdn.net/qq_26824159/article/details/125371936
            if (channel != null) {
                try {
                    channel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 写文件
     */
    private static void writeTest() {
        String text = "抱病齿发非，阅世城市换。\n" +
                "朋侪冢累累，在者亦云散。\n" +
                "穷居嬾出户，俯仰秋已半。\n" +
                "疏钟到倦枕，微火耿幽幔。\n" +
                "平生疑著处，忽若河冰泮。\n" +
                "百年寓逆旅，万事真既灌。\n" +
                "纷纷彼方寱，袖手不须唤。\n" +
                "萝月忽满窗，悠然付长叹。\n";

        String filePath = "C:\\Users\\Alex\\Desktop\\app\\write.txt";
        FileChannel channel = null;
        try {

            //文件不存在会自动创建
            channel = new RandomAccessFile(filePath, "rw").getChannel();
            ByteBuffer buffer = ByteBuffer.allocate(16);

            byte[] bytes = text.getBytes();
            for (int i = 0; i < bytes.length; i++) {

                //put：从position=0开始放
                buffer.put(bytes[i]);
                if (buffer.position() == buffer.limit() || i == bytes.length - 1) { // 缓存区已满或者已经遍历到最后一个字符
                    buffer.flip(); // 将缓冲区由写模式置为读模式
                    channel.write(buffer); // 将缓冲区的数据写到通道
                    buffer.clear(); // 清空缓存区，将缓冲区置为写模式，下次才能使用，position重置为0
                }
            }

            //filechannel.write 只是写入到了 PageCache，操作系统最终帮我们完成了 PageCache 到磁盘的最终写入
            //channel.force，强制把PageCache中的数据刷入磁盘
            channel.force(false);

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (channel != null) {
                try {
                    channel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 像BIO一样复制文件
     * <p>
     * 这样的复制不是零拷贝技术，仍然是像传统IO一样，先通过DMA拷贝复制内核缓存，再通过CPU拷贝复制到用户缓存
     * <p>
     * 整个read/write过程，经过4次线程上下文切换、2次DMA拷贝，2次CPU拷贝（CPU拷贝比DMA拷贝慢一个数量级）
     * <p>
     * 具体可参考 bio_.FileReadWriteTest
     * <p>
     * 阿里二面：什么是mmap？
     * https://mp.weixin.qq.com/s/sG0rviJlhVtHzGfd5NoqDQ
     */
    private static void copyTest() {
        String fromFile = "C:\\Users\\Alex\\Desktop\\app\\from.txt";
        String toFile = "C:\\Users\\Alex\\Desktop\\app\\to.txt";
        //使用try with resource代替try catch finally
        try (
                FileChannel fromChannel = new RandomAccessFile(fromFile, "rw").getChannel();
                final FileChannel toChannel = new RandomAccessFile(toFile, "rw").getChannel();
        ) {

            //定义固定大小的字节缓冲区
            final ByteBuffer buf = ByteBuffer.allocate(16);
            //从fromChannel通道中读数据到缓冲区中，当返回-1时，表示已经读到文件末尾
            int readBytes = fromChannel.read(buf);
            while (readBytes != -1) {
                //把缓冲区buf中的数据写到toChannel通道中
                buf.flip();//切换为读模式
                toChannel.write(buf);
                buf.clear();//清空缓冲区
                readBytes = fromChannel.read(buf);//继续读
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 测试零拷贝技术的实现方案之一：mmap
     * <p>
     * mmap，虚拟内存，也叫做内存映射，底层就是通过mmap指令把硬盘文件映射到内存中来
     * <p>
     * 具体的实现就是：内核读缓存区共享到用户缓冲中，省掉了从内核缓存到用户缓存的一次CPU拷贝，使用于大文件，系统限制1.5~2G之间
     * <p>
     * 对于用户来说，只需要操作虚拟内存mappedByteBuffer，put，get，虚拟内存和硬盘之间的交互有操作系统调度，处理过程对用户不可见
     * <p>
     * FileChannel.map，调用链路，FileChannelImpl.map0，底层CPU指定就是mmap指令
     * 参考：
     * 论最强IO：MappedByteBuffer VS FileChannel：https://blog.csdn.net/alex_xfboy/article/details/90174840
     * 对应JVM-HotSpot的linux源码：https://github.com/unofficial-openjdk/openjdk/blob/jdk8u/jdk8u/jdk/src/solaris/native/sun/nio/ch/FileChannelImpl.c
     * <p>
     * 关于mmap的介绍可参考：
     * 阿里二面：什么是mmap？https://mp.weixin.qq.com/s/sG0rviJlhVtHzGfd5NoqDQ
     * 【RocketMQ】MMAP零拷贝在RocketMQ中的运用：https://blog.csdn.net/u022812849/article/details/126007918
     * <p>
     * RocketMQ中数据的持久化落盘，查询出来给消费，都是使用mmap技术
     */
    private static void zeroCopyForMmapTest() {
        String filePath = "C:\\Users\\Alex\\Desktop\\app\\mmap.txt";
        try (
                FileChannel fileChannel = new RandomAccessFile(filePath, "rw").getChannel();
        ) {
            MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 4096);
            mappedByteBuffer.put("hello".getBytes(StandardCharsets.UTF_8));
            mappedByteBuffer.flip();
            byte[] bytes = new byte[5];
            mappedByteBuffer.get(bytes, 0, 5);
            System.out.println("content:" + new String(bytes, StandardCharsets.UTF_8));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 测试零拷贝技术的实现方案之二：sendfile
     * <p>
     * sendfile是一个CPU指令
     * <p>
     * FileChannel.transferTo()调用链，FileChannelImpl.transferTo0()，底层则是sendfile64
     * 参考：
     * 论最强IO：MappedByteBuffer VS FileChannel：https://blog.csdn.net/alex_xfboy/article/details/90174840
     * 对应JVM-HotSpot的linux源码：https://github.com/unofficial-openjdk/openjdk/blob/jdk8u/jdk8u/jdk/src/solaris/native/sun/nio/ch/FileChannelImpl.c
     * <p>
     * Linux2.1后，sendfile，只有2次线程上下文切换，2次DMA拷贝（从硬盘到内核缓存），1次CPU拷贝（从内核读缓存到内核Socket缓存）
     * Linux2.4后，sendfile Gather，添加新的硬件支持优化，内核socket缓冲区只保存内核读缓存的内存地址和文件描述符，省去了唯一的CPU拷贝
     * 参考：
     * 阿里二面：什么是mmap？https://mp.weixin.qq.com/s/sG0rviJlhVtHzGfd5NoqDQ
     * <p>
     * Kafka中消费数据使用了sendfile
     */
    private static void zeroCopyForSendfileTest() {
        String fromFile = "C:\\Users\\Alex\\Desktop\\app\\from.txt";
        String toFile = "C:\\Users\\Alex\\Desktop\\app\\to.txt";
        try (
                FileChannel fromChannel = new RandomAccessFile(fromFile, "rw").getChannel();
                final FileChannel toChannel = new RandomAccessFile(toFile, "rw").getChannel();
        ) {

            //三个参数：  从哪开始复制、复制多少、复制到哪个channel中
            fromChannel.transferTo(0, fromChannel.size(), toChannel);

            //另外还有个transferFrom方法，也差不多
            //详见NIO--FileChannel基本使用：读文件、写文件、文件传输：
            //https://blog.csdn.net/weixin_49702090/article/details/125325241

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
